package org.zebra.file.excel;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.IdUtil;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcel;
import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
import cn.idev.excel.write.handler.WriteHandler;
import cn.idev.excel.write.metadata.WriteSheet;
import cn.idev.excel.write.metadata.fill.FillConfig;
import cn.idev.excel.write.metadata.fill.FillWrapper;
import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.zebra.file.S3FileStorage;
import org.zebra.file.TemplateStorageType;
import org.zebra.file.excel.convert.ExcelBigNumberConvert;
import org.zebra.file.excel.core.*;
import org.zebra.file.excel.handler.DataWriteHandler;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * @author zhanghongbin
 */
public class SimpleExcel {

    private S3FileStorage s3FileStorage;

    public SimpleExcel(final S3FileStorage s3FileStorage) {
        this.s3FileStorage = s3FileStorage;
    }

    /**
     * 同步导入(适用于小数据量)
     *
     * @param is    输入流
     * @param clazz 类型
     * @param <T>   泛型
     * @return 转换后集合
     */
    public <T> List<T> importExcel(InputStream is, Class<T> clazz) {
        return FastExcel.read(is)
                .head(clazz)
                .ignoreEmptyRow(true)
                .autoCloseStream(false)
                .sheet()
                .doReadSync();
    }

    /**
     * 使用校验监听器 异步导入 同步返回
     *
     * @param is         输入流
     * @param clazz      对象类型
     * @param isValidate 是否 Validator 检验 默认为是
     * @param <T>        泛型
     * @return 转换后集合
     */
    public <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
        DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);
        FastExcel.read(is, clazz, listener).ignoreEmptyRow(true).sheet().doRead();
        return listener.getExcelResult();
    }

    /**
     * 使用自定义监听器 异步导入 自定义返回
     *
     * @param is       输入流
     * @param clazz    对象类型
     * @param listener 自定义监听器
     * @param <T>      泛型
     * @return 转换后集合
     */
    public <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
        FastExcel.read(is, clazz, listener).ignoreEmptyRow(true).sheet().doRead();
        return listener.getExcelResult();
    }

    /**
     * 导出excel
     *
     * @param collection 导出数据集合
     * @param sheetName  工作表的名称
     * @param clazz      实体类
     * @param response   响应体
     * @param <T>        泛型
     */
    public <T> void exportExcel(
            Collection<T> collection, String sheetName, Class<T> clazz, HttpServletResponse response) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(collection, sheetName, clazz, false, os, null, List.of());
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    public void exportEmptyExcel(String sheetName, HttpServletResponse response) {
        try {
            resetResponse(sheetName, response);
            ExcelWriter excelWriter = FastExcel.write(response.getOutputStream())
                    .autoCloseStream(false)
                    .build();
            WriteSheet writeSheet = FastExcel.writerSheet(0, sheetName).build();
            excelWriter.write(List.of(), writeSheet);
            excelWriter.finish();
        } catch (Exception e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param collection 导出数据集合
     * @param sheetName  工作表的名称
     * @param clazz      实体类
     * @param response   响应体
     * @param options    级联下拉选
     * @param <T>        泛型
     */
    public <T> void exportExcel(
            Collection<T> collection,
            String sheetName,
            Class<T> clazz,
            HttpServletResponse response,
            List<DropDownOptions> options) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(collection, sheetName, clazz, false, os, options, List.of());
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param collection 导出数据集合
     * @param sheetName  工作表的名称
     * @param clazz      实体类
     * @param merge      是否合并单元格
     * @param response   响应体
     * @param <T>        泛型
     */
    public <T> void exportExcel(
            Collection<T> collection, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(collection, sheetName, clazz, merge, os, null, List.of());
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param collection 导出数据集合
     * @param sheetName  工作表的名称
     * @param clazz      实体类
     * @param merge      是否合并单元格
     * @param response   响应体
     * @param options    级联下拉选
     * @param <T>        泛型
     */
    public <T> void exportExcel(
            Collection<T> collection,
            String sheetName,
            Class<T> clazz,
            boolean merge,
            HttpServletResponse response,
            List<DropDownOptions> options) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(collection, sheetName, clazz, merge, os, options, List.of());
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param collection 导出数据集合
     * @param sheetName  工作表的名称
     * @param clazz      实体类
     * @param os         输出流
     * @param <T>        泛型
     */
    public <T> void exportExcel(Collection<T> collection, String sheetName, Class<T> clazz, OutputStream os) {
        exportExcel(collection, sheetName, clazz, false, os, null, List.of());
    }

    /**
     * 导出excel
     *
     * @param collection 导出数据集合
     * @param sheetName  工作表的名称
     * @param clazz      实体类
     * @param os         输出流
     * @param options    级联下拉选内容
     * @param <T>        泛型
     */
    public <T> void exportExcel(
            Collection<T> collection,
            String sheetName,
            Class<T> clazz,
            OutputStream os,
            List<DropDownOptions> options) {
        exportExcel(collection, sheetName, clazz, false, os, options, List.of());
    }

    /**
     * 导出excel
     *
     * @param collection       导出数据集合
     * @param sheetName        工作表的名称
     * @param clazz            实体类
     * @param merge            是否合并单元格
     * @param os               输出流
     * @param options          下拉项
     * @param writeHandlerList handlerList
     * @param <T>              泛型
     */
    public <T> void exportExcel(
            Collection<T> collection,
            String sheetName,
            Class<T> clazz,
            boolean merge,
            OutputStream os,
            List<DropDownOptions> options,
            List<WriteHandler> writeHandlerList) {
        ExcelWriterSheetBuilder builder = FastExcel.write(os, clazz)
                .autoCloseStream(false)
                // 自动适配
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new DataWriteHandler(clazz))
                // 大数值自动转换 防止失真
                .registerConverter(new ExcelBigNumberConvert())
                .sheet(sheetName);
        if (merge) {
            // 合并处理器
            builder.registerWriteHandler(new CellMergeStrategy(collection, true));
        }
        if (!writeHandlerList.isEmpty()) {
            writeHandlerList.forEach(writeHandler -> builder.registerWriteHandler(writeHandler));
        }
        // 添加下拉框操作
        builder.registerWriteHandler(new ExcelDownHandler(options));
        builder.doWrite(collection);
    }

    /**
     * 单表多数据模板导出 模板格式为 {.属性}
     *
     * @param filename            文件名
     * @param templatePath        模板路径 resource 目录下的路径包括模板文件名
     *                            例如: excel/temp.xlsx
     *                            重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param templateStorageType 模版存储类型
     * @param data                模板需要的数据
     * @param response            响应体
     */
    public void exportTemplate(
            Collection<Object> data,
            String filename,
            String templatePath,
            TemplateStorageType templateStorageType,
            HttpServletResponse response) {
        try {
            resetResponse(filename, response);
            ServletOutputStream os = response.getOutputStream();
            exportTemplate(data, templatePath, templateStorageType, os);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 单表多数据模板导出 模板格式为 {.属性}
     *
     * @param templatePath        模板路径 resource 目录下的路径包括模板文件名
     *                            例如: excel/temp.xlsx
     *                            重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param templateStorageType 模版存储类型
     * @param data                模板需要的数据
     * @param os                  输出流
     */
    public void exportTemplate(
            Collection<Object> data, String templatePath, TemplateStorageType templateStorageType, OutputStream os) {
        if (CollUtil.isEmpty(data)) {
            throw new IllegalArgumentException("数据为空");
        }
        ExcelWriter excelWriter = FastExcel.write(os)
                .withTemplate(getInputStream(templatePath, templateStorageType))
                .autoCloseStream(false)
                // 大数值自动转换 防止失真
                .registerConverter(new ExcelBigNumberConvert())
                .registerWriteHandler(
                        new DataWriteHandler(data.iterator().next().getClass()))
                .build();
        WriteSheet writeSheet = FastExcel.writerSheet().build();
        // 单表多数据导出 模板格式为 {.属性}
        for (Object d : data) {
            excelWriter.fill(d, writeSheet);
        }
        excelWriter.finish();
    }

    /**
     * 多表多数据模板导出 模板格式为 {key.属性}
     *
     * @param filename            文件名
     * @param templatePath        模板路径 resource 目录下的路径包括模板文件名
     *                            例如: excel/temp.xlsx
     *                            重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param templateStorageType 模版存储类型
     * @param data                模板需要的数据
     * @param response            响应体
     */
    public void exportTemplateMultiList(
            Map<String, Object> data,
            String filename,
            String templatePath,
            TemplateStorageType templateStorageType,
            HttpServletResponse response) {
        try {
            resetResponse(filename, response);
            ServletOutputStream os = response.getOutputStream();
            exportTemplateMultiList(data, templatePath, templateStorageType, os);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 多sheet模板导出 模板格式为 {key.属性}
     *
     * @param filename            文件名
     * @param templatePath        模板路径 resource 目录下的路径包括模板文件名
     *                            例如: excel/temp.xlsx
     *                            重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param templateStorageType 模版存储类型
     * @param data                模板需要的数据
     * @param response            响应体
     */
    public void exportTemplateMultiSheet(
            List<Map<String, Object>> data,
            String filename,
            String templatePath,
            TemplateStorageType templateStorageType,
            HttpServletResponse response) {
        try {
            resetResponse(filename, response);
            ServletOutputStream os = response.getOutputStream();
            exportTemplateMultiSheet(data, templatePath, templateStorageType, os);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 多表多数据模板导出 模板格式为 {key.属性}
     *
     * @param templatePath        模板路径 resource 目录下的路径包括模板文件名
     *                            例如: excel/temp.xlsx
     *                            重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param templateStorageType 模版存储类型
     * @param data                模板需要的数据
     * @param os                  输出流
     */
    public void exportTemplateMultiList(
            Map<String, Object> data, String templatePath, TemplateStorageType templateStorageType, OutputStream os) {
        if (CollUtil.isEmpty(data)) {
            throw new IllegalArgumentException("数据为空");
        }
        ExcelWriter excelWriter = FastExcel.write(os)
                .withTemplate(getInputStream(templatePath, templateStorageType))
                .autoCloseStream(false)
                // 大数值自动转换 防止失真
                .registerConverter(new ExcelBigNumberConvert())
                .build();
        WriteSheet writeSheet = FastExcel.writerSheet().build();

        for (Map.Entry<String, Object> map : data.entrySet()) {
            // 设置列表后续还有数据
            FillConfig fillConfig =
                    FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            if (map.getValue() instanceof Collection) {
                // 多表导出必须使用 FillWrapper
                excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
            } else {
                excelWriter.fill(map.getValue(), writeSheet);
            }
        }
        excelWriter.finish();
    }

    /**
     * 多sheet模板导出 模板格式为 {key.属性}
     *
     * @param templatePath        模板路径 resource 目录下的路径包括模板文件名
     *                            例如: excel/temp.xlsx
     *                            重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param templateStorageType 模版存储类型
     * @param data                模板需要的数据
     * @param os                  输出流
     */
    public void exportTemplateMultiSheet(
            Collection<Map<String, Object>> data,
            String templatePath,
            TemplateStorageType templateStorageType,
            OutputStream os) {
        ExcelWriter excelWriter = FastExcel.write(os)
                .withTemplate(getInputStream(templatePath, templateStorageType))
                .autoCloseStream(false)
                // 大数值自动转换 防止失真
                .registerConverter(new ExcelBigNumberConvert())
                .build();
        if (CollUtil.isEmpty(data)) {
            throw new IllegalArgumentException("数据为空");
        }
        for (int i = 0; i < data.size(); i++) {
            WriteSheet writeSheet = FastExcel.writerSheet(i).build();
            for (Map.Entry<String, Object> map : CollUtil.get(data, i).entrySet()) {
                // 设置列表后续还有数据
                FillConfig fillConfig =
                        FillConfig.builder().forceNewRow(Boolean.TRUE).build();
                if (map.getValue() instanceof Collection) {
                    // 多表导出必须使用 FillWrapper
                    excelWriter.fill(
                            new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
                } else {
                    excelWriter.fill(map.getValue(), writeSheet);
                }
            }
        }
        excelWriter.finish();
    }

    private InputStream getInputStream(String templatePath, TemplateStorageType templateStorageType) {
        if (templateStorageType == TemplateStorageType.CLASSPATH) {
            ClassPathResource templateResource = new ClassPathResource(templatePath);
            return templateResource.getStream();
        } else {
            String[] templatePathArray = templatePath.split(":");
            String buckName = "";
            String objectName;
            if (templatePathArray.length == 1) {
                objectName = templatePath;
            } else {
                buckName = templatePathArray[0];
                objectName = templatePathArray[1];
            }
            if (this.s3FileStorage == null) {
                throw ExceptionUtil.wrapRuntime("s3FileStorage 对象为空");
            }
            return this.s3FileStorage.getInputStream(buckName, objectName);
        }
    }

    private void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
        String filename = encodingFilename(sheetName);
        setAttachmentResponseHeader(response, filename);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
    }

    private void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
        String percentEncodedFileName = percentEncode(realFileName);
        String contentDispositionValue = String.format(
                "attachment; filename=%s;filename*=utf-8''%s", percentEncodedFileName, percentEncodedFileName);
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
        response.setHeader("Content-disposition", contentDispositionValue);
        response.setHeader("download-filename", percentEncodedFileName);
    }

    /**
     * 百分号编码工具方法
     *
     * @param s 需要百分号编码的字符串
     * @return 百分号编码后的字符串
     */
    private String percentEncode(String s) {
        String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
        return encode.replaceAll("\\+", "%20");
    }

    public String encodingFilename(String filename) {
        return filename + "_" + IdUtil.fastSimpleUUID() + ".xlsx";
    }
}
